package com.lyb.hardware.disk;

import com.lyb.Utils;
import com.lyb.constant.DiskConstant;
import com.lyb.constant.SystemConstant;
import com.lyb.hardware.AlpacaMainBoard;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
 * 目录类
 *
 * @author Mr.Alpaca potato
 * @version 1.4.0
 */
public class AlpacaDirectory {
    /**
     * 二进制格式的目录项内容
     */
    private byte[] content;
    /**
     * 目录项对象数组
     */
    private DirectoryEntry[] directoryEntries;
    /**
     * 目录路径
     */
    private String directoryPath;
    /**
     * 主板
     */
    private final AlpacaMainBoard mainBoard;
    /**
     * 该目录占用的磁盘号
     */
    private byte diskNum;

    public AlpacaDirectory() {
        this.directoryEntries = new DirectoryEntry[DiskConstant.DIRECTORY_ENTRY_LENGTH];
        /* 获取主板对象，通过主板提供的磁盘API对磁盘进行操作 */
        this.mainBoard = SystemConstant.MAIN_BOARD;
    }

    /**
     * 根据传入二进制内容构造对象
     *
     * @param content 文件内容
     */
    public AlpacaDirectory(byte[] content) {
        this();
        this.content = content;
        /* 将二进制内容解析成目录项对象数组 */
        parseContent();
    }

    /**
     * 目录路径
     *
     * @param directoryPath 路径
     */
    public AlpacaDirectory(String directoryPath) {
        this();
        this.directoryPath = directoryPath;
    }

    /**
     * 解析字节数组，将二进制内容转换成目录项数组
     */
    private void parseContent() {
        for (int i = 0; i < 8; i++) {
            directoryEntries[i] = new DirectoryEntry(Arrays.copyOfRange(content, i * 8, i * 8 + 8));
        }
    }

    /**
     * 查找文件/目录，根据传入文件/目录名和类型在目录项列表中中查找相同名字的文件/目录
     * 查找成功返回该文件目录项对象，失败返回null
     *
     * @param fileName 要查找的文件/目录名
     * @param fileType 要查找的文件/目录类型
     * @return 目录项对象
     */
    public DirectoryEntry findDirectoryEntry(String fileName, String fileType) {
        /* 遍历目录项列表，若有目录项信息与传入参数匹配则返回该目录项 */
        for (DirectoryEntry entry : this.directoryEntries) {
            if (entry.getName().equals(fileName) && entry.getType().equals(fileType)) {
                return entry;
            }
        }
        return null;
    }

    /**
     * 创建目录，逻辑：
     * 1.判断目录是否已存在
     * 2.判断父目录是否均已创建
     * 3.获取磁盘空间作为该目录的使用空间
     * 4.生成目录项并写入父目录中
     *
     * @return 创建结果
     */
    public boolean build() {
        if (exists()) {
            Utils.outCommandResult("[系统|磁盘]: 目录已存在于磁盘中");
            return false;
        }

        String[] path = this.directoryPath.split("/");
        /* 从根目录开始寻找 */
        AlpacaDirectory directory = new AlpacaDirectory(mainBoard.getRootDirectoryFromDisk());
        DirectoryEntry entry = null;

        /* 父目录索引范围 */
        int lastIndex = path.length - 1;
        /* 依次查找父目录目录是否已存在，不存在则直接返回错误 */
        for (int i = 0; i < lastIndex; i++) {
            entry = directory.findDirectoryEntry(path[i], " ");
            if (entry == null) {
                Utils.outCommandResult("[系统|磁盘]: 父目录 " + path[i] + " 不存在");
                return false;
            }

            try {
                /* 将该目录磁块读出作为下次遍历的起始目录 */
                directory = new AlpacaDirectory(mainBoard.readBlockFromDisk(entry.getStartDiskNum()));
            } catch (IOException e) {
                Utils.outCommandResult("[系统|磁盘]: 目录读取失败！！！");
                return false;
            }
        }

        /* 获取目录能占据的空磁块 */
        byte num = mainBoard.getFreeBlockNumFromDisk();
        if (num == -1) {
            Utils.outCommandResult("[系统|磁盘]: 磁盘已无空闲空间");
            return false;
        }

        /* 新建目录占据一个磁块(无内容写入) */
        mainBoard.occupyBlockFromDisk(num);
        /* 取出父目录所在磁块内容并写入新创建的目录项 */
        DirectoryEntry subEntry = new DirectoryEntry(path[lastIndex], " ", SystemConstant.FILE_WRITE, 0, num);
        assert entry != null;
        byte fatherDirectoryDiskNum = entry.getStartDiskNum();
        try {
            /* 将新创建的目录项写入父目录磁块中 */
            writeToFatherDirectory(fatherDirectoryDiskNum, subEntry);
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 父目录无存储空间");
            return false;
        }
        return true;
    }

    /**
     * 判断目录是否已存在于磁盘中
     *
     * @return 判断结果
     */
    public boolean exists() {
        this.directoryPath = this.directoryPath.replace("\\", "/");
        String[] directoryPath = this.directoryPath.split("/");

        /* 从根目录开始寻找 */
        AlpacaDirectory directory = new AlpacaDirectory(mainBoard.getRootDirectoryFromDisk());
        DirectoryEntry entry = null;
        /* 依次查找目录是否已存在 */
        for (String dir : directoryPath) {
            entry = directory.findDirectoryEntry(dir, " ");
            if (entry == null) {
                return false;
            }

            try {
                /* 将该目录磁块读出作为下次遍历的起始目录 */
                directory = new AlpacaDirectory(mainBoard.readBlockFromDisk(entry.getStartDiskNum()));
            } catch (IOException e) {
                Utils.outCommandResult("[系统|磁盘]: 目录读取失败！！！");
                return false;
            }
        }
        /* 获取该目录占用的磁盘号和包含的目录项 */
        assert entry != null;
        this.diskNum = entry.getStartDiskNum();
        this.directoryEntries = directory.getDirectoryEntries();
        return true;
    }

    /**
     * 将当前目录项写入父目录磁块的空部分里
     *
     * @throws IOException 读取磁块出现异常或父目录已填满
     */
    private void writeToFatherDirectory(byte fatherDiskNum, DirectoryEntry entry) throws IOException {
        /* 取出父目录磁块内容 */
        byte[] oldContent = mainBoard.readBlockFromDisk(fatherDiskNum);
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);

        byte[] subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];

        /* 设置标志位判断新目录项是否已经写入 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(subDirectoryEntry);

            /* 若新目录项还没有写入且读出二进制内容起始磁盘号为0（说明该部分没有内容），则将新目录项写入原数组 */
            if (!operationFlag && subDirectoryEntry[7] == 0) {
                /* 获取目录项的二进制内容(创建对象时已生成) */
                subDirectoryEntry = entry.getDirectoryEntry();
                operationFlag = true;
            }

            newBuffer.put(subDirectoryEntry);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 如果新目录项没有执行写入磁块，则说明磁块已经满了 */
        if (!operationFlag) {
            Utils.outCommandResult("[系统|磁盘]: 父目录已填满，目录写入失败");
            throw new IOException("error");
        }

        /* 将目录项内容写回磁盘 */
        mainBoard.writeBlockToDisk(fatherDiskNum, newBuffer.array());
    }

    /**
     * 删除空目录，逻辑：
     * 1.判断目录是否已存在
     * 2.判断目录是否为根目录
     * 3.判断目录内容是否为空
     * 4.将目录项从父目录中删除
     * 5.释放磁盘空间
     *
     * @return 删除结果
     */
    public boolean remove() {
        /* 判断目录是否已存在 */
        if (!exists()) {
            Utils.outCommandResult("[系统|磁盘]: 目录不存在于磁盘中");
            return false;
        }

        /* 目录若为根目录则禁止删除 */
        if (this.diskNum <= 10) {
            Utils.outCommandResult("[系统|磁盘]: 磁盘根目录区域不得删除！");
            return false;
        }

        /* 读出目录使用磁块内容 */
        AlpacaDirectory directory = null;
        try {
            /* 当前目录所占磁块号在判断目录是否存在时已获取 */
            directory = new AlpacaDirectory(mainBoard.readBlockFromDisk(this.diskNum));
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录内容读取失败");
            return false;
        }

        /* 若目录内容为空则从父目录中删除该目录项并释放磁盘空间 */
        if (directory.isEmpty()) {
            try {
                /* 从父目录中删除 */
                deleteDirectoryEntryInFather(this.directoryPath);
            } catch (IOException e) {
                Utils.outCommandResult("[系统|磁盘]: 目录项无法从父目录中删除");
                return false;
            }
            /* 释放磁盘空间 */
            mainBoard.freeBlockFromDisk(this.diskNum);
            return true;
        }
        Utils.outCommandResult("[系统|磁盘]: 该目录不为空");
        return false;
    }

    /**
     * 判断当前目录下是否为空(没有子文件和子目录)
     * 逻辑：磁块内容是否全为空字节(0)
     *
     * @return 判断结果
     */
    private boolean isEmpty() {
        for (byte sub : this.content) {
            if (sub != 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * 从父目录中删除该目录的目录项
     *
     * @param directoryPath 目录路径
     * @throws IOException 读取父目录磁盘内容失败
     */
    private void deleteDirectoryEntryInFather(String directoryPath) throws IOException {
        /* 获取父目录磁块号 */
        byte fatherDiskNum = getFatherDirectoryDiskNum(directoryPath);

        /* 读取父目录磁块内容 */
        byte[] oldContent = mainBoard.readBlockFromDisk(fatherDiskNum);
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);
        /* 开辟子目录项缓冲 */
        byte[] subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];

        /* 切分路径名 */
        String[] path = directoryPath.split("/");
        /* 获取目录名 */
        String name = path[path.length - 1];
        /* 操作标志 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(subDirectoryEntry);

            /* 若目录项已删除了则后续目录项写回即可 */
            if (!operationFlag) {
                DirectoryEntry entry = new DirectoryEntry(subDirectoryEntry);
                /* 若目录项与期望相同则直接将该目录项清空 */
                if (entry.getName().equals(name) && " ".equals(entry.getType())) {
                    subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];
                    operationFlag = true;
                }
            }

            newBuffer.put(subDirectoryEntry);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 将目录项内容写回磁盘 */
        mainBoard.writeBlockToDisk(fatherDiskNum, newBuffer.array());
    }

    /**
     * 获取指定目录的父目录磁块号，即存放当前目录的目录项的磁块号
     * 该方法只会在当前目录存在父目录项时被调用
     *
     * @param path 获取父目录磁块号的目录路径
     * @return 磁块号
     * @throws IOException 父目录磁块内容读取异常
     */
    private byte getFatherDirectoryDiskNum(String path) throws IOException {
        String[] directoryPath = path.split("/");

        /* 从根目录开始寻找 */
        AlpacaDirectory directory = new AlpacaDirectory(mainBoard.getRootDirectoryFromDisk());
        DirectoryEntry entry = null;
        int fatherIndex = directoryPath.length - 1;
        /* 依次查找目录是否已存在 */
        for (int i = 0; i < fatherIndex; i++) {
            entry = directory.findDirectoryEntry(directoryPath[i], " ");
            /* 将该目录磁块读出作为下次遍历的起始目录 */
            directory = new AlpacaDirectory(mainBoard.readBlockFromDisk(entry.getStartDiskNum()));
        }
        /* 获取父目录占用的磁盘号 */
        assert entry != null;
        return entry.getStartDiskNum();
    }

    /**
     * 删除目录(目录内容可以不为空)
     *
     * @return 删除结果
     */
    public boolean delete() {
        /* 判断目录是否已存在 */
        if (!exists()) {
            Utils.outCommandResult("[系统|磁盘]: 目录不存在于磁盘中");
            return false;
        }

        /* 目录若为根目录则禁止删除 */
        if (this.diskNum <= 10) {
            Utils.outCommandResult("[系统|磁盘]: 磁盘根目录区域不得删除！");
            return false;
        }

        /* 读出目录使用磁块内容 */
        AlpacaDirectory directory = null;
        try {
            /* 当前目录所占磁块号在判断目录是否存在时已获取 */
            directory = new AlpacaDirectory(mainBoard.readBlockFromDisk(this.diskNum));
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录内容读取失败");
            return false;
        }
        return delete(directory, this.directoryPath);
    }

    /**
     * 递归删除目录及内容
     *
     * @param directory 目录
     * @param path      递归过程的当前目录，用于拼接获取文件完整路径
     * @return 删除结果
     */
    private boolean delete(AlpacaDirectory directory, String path) {
        /* 遍历该目录的磁块内容 */
        for (DirectoryEntry entry : directory.getDirectoryEntries()) {
            /* 目录项为空则跳过(磁块号为0) */
            if (entry.getStartDiskNum() == 0) {
                break;
            } else if (!DiskConstant.DIRECTORY_TYPE.equals(entry.getType())) { /* 若目录项指向文件则直接删除该文件 */
                /* 拼接获取文件名 */
                String fileName = path + "/" + entry.getName() + "." + entry.getType();
                /* 调用文件类 API 删除文件*/
                boolean flag = new AlpacaFile(fileName).delete();
                Utils.outCommandResult("[系统|磁盘]: 目录中文件 " + fileName + (flag ? " 删除成功" : " 删除失败"));
                if (!flag) {
                    return false;
                }
            } else { /* 若目录项指向目录则递归 */
                try {
                    String subPath = path + "/" + entry.getName();
                    boolean flag = delete(new AlpacaDirectory(mainBoard.readBlockFromDisk(entry.getStartDiskNum())), subPath);
                    Utils.outCommandResult("[系统|磁盘]: 子目录 " + subPath + (flag ? " 删除成功" : " 删除失败"));
                    if (!flag) {
                        return false;
                    } else {
                        /* 从父目录中删除该目录项并释放磁盘空间 */
                        try {
                            /* 从父目录中删除 */
                            deleteDirectoryEntryInFather(subPath);
                        } catch (IOException e) {
                            Utils.outCommandResult("[系统|磁盘]: 目录项 " + subPath + " 无法从父目录中删除");
                            return false;
                        }
                    }
                } catch (IOException e) {
                    Utils.outCommandResult("[系统|磁盘]: 目录读取发生异常");
                    return false;
                }
            }
        }

        /* 从父目录中删除当前目录项并释放磁盘空间 */
        try {
            /* 从父目录中删除 */
            deleteDirectoryEntryInFather(path);
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录项 " + path + " 无法从父目录中删除");
            return false;
        }
        /* 释放磁盘空间 */
        mainBoard.freeBlockFromDisk(this.diskNum);
        return true;
    }

    /**
     * 添加目录项
     * 在当前目录所占磁盘下添加一个新的目录项
     *
     * @param entry 需添加的目录项
     * @return 添加结果
     */
    public boolean addDirectoryEntry(DirectoryEntry entry) {
        /* 1.判断是否存在该目录项(目录列表在判断目录存在时已添加) */
        if (findDirectoryEntry(entry.getName(), entry.getType()) != null) {
            Utils.outCommandResult("[系统|磁盘]: 目录项 " + entry.getName() + " 已存在于目录中");
            return false;
        }

        /* 2.在磁块中添加该目录项 */
        /* 读取磁块内容 */
        byte[] oldContent = new byte[0];
        try {
            oldContent = mainBoard.readBlockFromDisk(this.diskNum);
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录内容读取失败");
            return false;
        }
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);
        /* 开辟子目录项缓冲 */
        byte[] entryBuffer = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];
        /* 设置标志位判断新目录项是否已经写入 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(entryBuffer);

            /* 若新目录项还没有写入且读出二进制内容起始磁盘号为0（说明该部分没有内容），则将新目录项写入原数组 */
            if (!operationFlag && entryBuffer[7] == 0) {
                entry.setDirectoryEntry();
                entryBuffer = entry.getDirectoryEntry();
                operationFlag = true;
            }

            newBuffer.put(entryBuffer);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 如果新目录项没有执行写入磁块，则说明磁块已经满了 */
        if (!operationFlag) {
            Utils.outCommandResult("[系统|磁盘]: 父目录已填满，目录写入失败");
            return false;
        }

        /* 将目录项内容写回磁盘 */
        try {
            mainBoard.writeBlockToDisk(this.diskNum, newBuffer.array());
            return true;
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录内容写回失败");
            return false;
        }
    }

    /**
     * 删除目录项
     * 在当前目录所占磁盘下删除指定目录项
     *
     * @param entry 需删除的目录项
     * @return 删除结果
     */
    public boolean deleteDirectoryEntry(DirectoryEntry entry) {
        /* 1.判断是否存在该目录项(目录列表在判断目录存在时已添加) */
        if (findDirectoryEntry(entry.getName(), entry.getType()) == null) {
            Utils.outCommandResult("[系统|磁盘]: 目录项 " + entry.getName() + "不存在于目录中");
            return false;
        }

        /* 2.从磁块中删除该目录项 */
        /* 读取磁块内容 */
        byte[] oldContent = new byte[0];
        try {
            oldContent = mainBoard.readBlockFromDisk(this.diskNum);
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录内容读取失败");
            return false;
        }
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);
        /* 开辟子目录项缓冲 */
        byte[] entryBuffer = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];
        /* 操作标志 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(entryBuffer);

            /* 若目录项已删除了则后续目录项写回即可 */
            if (!operationFlag) {
                DirectoryEntry temp = new DirectoryEntry(entryBuffer);
                /* 若目录项与期望相同则直接将该目录项清空 */
                if (entry.getName().equals(temp.getName()) && entry.getType().equals(temp.getType())) {
                    entryBuffer = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];
                    operationFlag = true;
                }
            }

            newBuffer.put(entryBuffer);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 将目录项内容写回磁盘 */
        try {
            mainBoard.writeBlockToDisk(this.diskNum, newBuffer.array());
            return true;
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 目录内容写回失败");
            return false;
        }
    }


    /* ********** Getter & Setter ********** */

    public void setDiskNum(byte diskNum) {
        this.diskNum = diskNum;
    }

    public DirectoryEntry[] getDirectoryEntries() {
        return directoryEntries;
    }
}
